package ihm.models;

import java.util.List;

import ihm.models.events.CharacterChangedEvent;
import ihm.models.events.EquipmentChangedEvent;
import ihm.models.events.ItemTypeChangedEvent;
import ihm.models.events.LanguageChangedEvent;
import ihm.models.events.QueryChangedEvent;
import ihm.models.events.StatChangedEvent;
import ihm.views.options.ATKjOptionPane;

import javax.swing.event.EventListenerList;

import systems.configurations.SystemConfiguration;
import systems.database.DataBaseHandler;
import systems.database.query.Query;
import systems.database.query.SelectQuery;
import systems.database.query.condition.ComparatorEnum;
import systems.database.query.condition.Condition;
import systems.database.query.condition.Expression;
import systems.database.query.condition.JunctionEnum;
import systems.database.query.condition.Literal;
import systems.exceptions.equipments.CanNotEquipThisItem;
import systems.languages.MultiLanguages;
import systems.rpgBuilder.Classe;
import systems.rpgBuilder.Faction;
import systems.rpgBuilder.Item;
import systems.rpgBuilder.RpgBuilder;
import characters.Characters;
import equipments.Equipment;
import equipments.Items;
import equipments.enumeration.EquipmentSlots;

/**
 * This class provides a model for the ATK program.
 * According to the M-V-C design pattern a Model is used to manage information
 * and notify observers when that information changes.
 * The model is the domain-specific representation of the data upon which 
 * the application operates.
 * When a model changes its state, it notifies its associated views so they can
 * be refreshed.
 * Here is the principal data that ATK program contains :
 * <ul>
 *  <li>System Configuration [SystemConfiguration Instance]</li>
 *  <li>MultiLanguage handler [MultiLanguage Instance]</li>
 *  <li>Database Handler [DatabaseHandler
 *  <li>The current language used [String]</li>
 *  <li>A character [Character]</li>
 *  <li>Character's equipment [Equipment]</li>
 *  <li>The item selected in the item tree [Item]</li>
 *  <li>The EventListener list [EventListenerList]</li>
 * </ul>
 * @author Baptiste Legrand & Benjamin Orman
 * @version 3.0
 */
public class ATKmodel
{
  //Software configuration sytstem
  private SystemConfiguration sysConf = SystemConfiguration.getInstance();
  //Multilanguage Handler
  private MultiLanguages languageHandler = MultiLanguages.getInstance();
  //Classe list
  private List<Classe> classesList = RpgBuilder.getClassesList();
  //The Character
  private Characters  character;
  //The Character's Equipment
  private Equipment   equipement;
  //Faction list
  private List<Faction> factionsList = RpgBuilder.getFactionsList();
  //Model file name
  private String fileName;
  //Path of the file opened
  private String filePath;
  //Boolean used to know if the model has been modify since its creation/opening
  private boolean dirty = false;
  //Event Listener List
  private EventListenerList listeners = new EventListenerList();
  //The query use to do something on the DataBase.
  private Query query = new SelectQuery();
  //Selected item in the Item Tree
  private Item treeSelectedItem;
  //Name of the current language 
  private String currentLanguage;
  
  /**
   * Model constructor. This constructor call the initModel method to 
   * initialize the model with a new character.
   */
  public ATKmodel()
  {
    this.currentLanguage = this.languageHandler.getLanguageFileName();
    initModel(null);
  }

  /**
   * This method provides to initialize the model according to the Characters
   * given in paramater.
   * If the parameter is "null", then this method will create a default 
   * character.
   * @param newCharacter - The character to use in the model.
   */
  public void initModel(Characters newCharacter)
  {
    java.sql.Statement transmission = DataBaseHandler.getStatement();
    if(newCharacter == null)
    {
      newCharacter = new Characters();
      newCharacter.setClasse(classesList.get(0));
      newCharacter.setFaction(factionsList.get(0));
      newCharacter.setLevel(1);
      newCharacter.setName(MultiLanguages.getProperties("model.defaultCharacter.name"));
      newCharacter.initCharacterStat(transmission);
    }
    //Update the character and the equipment store in the model.
    this.setCharacter(newCharacter);
    this.dirty = false;
  }

  /**
   * This function is used to set the new language of the program.
   * When you set a new language, all the listener register will be notice
   * that the language changed.
   * @param newLanguage - The new language which will be used in the program.
   */
  public void setLanguage(String newLanguage)
  {
    //Save the new language
    this.currentLanguage = newLanguage;
    //Load the language file
    MultiLanguages.setLanguage(newLanguage);
    //Notice all the listener
    this.fireLanguageChanged();
    //We changed the model
    this.dirty = true;
  }
  /**
   * This function returns the current language of the program.
   * @return String - The current language used in the program.
   */
  public String getLanguage()
  {
    return this.currentLanguage;
  }

  /**
   * This function is used to set a new character and update
   * the equipment store in the model by the new character's equipments.
   * This function also notice all the listener that the character changed
   * (i.e : his equipment and his statistics changed, then these change are 
   * notice to the listeners). 
   * @param newCharacter - The new character.
   */
  public void setCharacter(Characters newCharacter)
  {
    System.out.println("[Model] - Set Character");
    System.out.println("[Model] -character mastery list size: "+newCharacter.getMasteryList().size());
    //Store the new character
    this.character = newCharacter;
    if(newCharacter.getEquipment().getCharacter() == null)
      newCharacter.getEquipment().setCharacter(newCharacter);
    //Notice all the listener that the character changed.
    fireCharacterChangedEvent();
    //Update the equipment store in the model by the new character's one.
    //Note: This method notice all the listener that the equipment changed
    //, then it update the stat and fire the new stat.
    this.setEquipments(this.character.getEquipment());
    //We changed the model
    this.dirty = true;
  }

  /**
   * This function returns the character currently used.
   * @return Characters - The character currently used.
   */
  public Characters getCharacter()
  {
    System.out.println("[Model] - Get Character");
    return this.character;
  }

  /**
   * This function is used to set a new equipment to the character.
   * Moreover, this method update the equipment store in the model and
   * notice all the listeners that the equipment changed.
   * Since, modify the equipment modify the character's statistics, the
   * setEquipment method notice all the listener that the stats changed.
   * @param newEquipment - The character's new equipment.
   */
  public void setEquipments(Equipment newEquipment)
  {
    System.out.println("[Model] - Set Equipment");
    //Store the new equipment into the model
    this.equipement = newEquipment;
    //update the character's equipment with the new one.
    this.character.setEquipment(newEquipment);
    //notice all the listener that the character's equipment changed.
    fireEquipmentChangedEvent();
    //notice all the listener that the character's statistics changed.
    fireStatChangedEvent();
    //We changed the model
    this.dirty = true;
  }
  
  /**
   * This methode provides mechanism to change a part of the character's
   * equipment. When a part is changed, the new character's equipment is
   * store in the model.
   * Since the equipment (and, maybe, the statistics) changed, all the
   * listeners will be aware of these changes.
   * @param slot - The EquipmentSlots where to equip the items
   * @param equipmentPart - The Items which will equip the characters.
   * @throws CanNotEquipThisItem - This exception will be throws if you
   * are trying to equip an item into a wrong place.
   * @see CanNotEquipThisItem class for more details about this error.
   */
  public void setEquipments(EquipmentSlots slot, Items equipmentPart)
  throws CanNotEquipThisItem
  {
    System.out.println("[Model] - Set part of an equipment");
    //Update the character's equipment
    this.character.updateEquipment(slot, equipmentPart);
    //Store the new character's equipment in the model
    this.equipement = this.character.getEquipment();
    //notice all the listener that the character's equipment changed.
    this.fireEquipmentChangedEvent();
    //notice all the listener that the character's statistics changed.
    this.fireStatChangedEvent();
    //We changed the model
    this.dirty = true;
  }

  /**
   * Returns the character's equipments store into the model.
   * @return Equipment - The character's equipment store into the model.
   */
  public Equipment getEquipment()
  {
    return this.equipement;
  }

  /**
   * Setter used to update the item selected in the tree according to the Item
   * given in parameter and notify all the listener.
   * As the selected item is used to display the list of item into the 
   * catalogPanel, this method update the query and notify this change.
   * @param newItem - The Item which is selected in the tree item.
   */
  public void setTreeSelectedItem(Item newItem)
  {
    System.out.println("[Model] - Set Tree Selected Item");
    //Update the item selected in the tree
    this.treeSelectedItem = newItem;
    //Update the query according to the new item selected.
    //As you can gess, the newItem could not be null since it is given by
    //the HMI which controle the "null" case. 
    this.query.setTableName(newItem.getTableName());
    //Create the where clause according to the newItem.
    this.query.setWhereCondition(this.buildItemSelectedCondition());
    //Notify all the listener that the item changed.
    this.fireItemTypeChangedEvent();
    //Notify all the listener that the query changed.
    this.fireQueryChanged();
  }

  /**
   * Add an ATKlistener to the listener list.
   * @param newListener - The new ATKlistener to register.
   * @see ATKlistener for more information.
   */
  public void addATKlistener(ATKlistener newListener)
  {
    System.out.println("[Model] - Ajout d'un listener au model");
    this.listeners.add(ATKlistener.class, newListener);
  }
  
  /**
   * Try to remove an ATKlistener previously register.
   * @param listenerToRemove - The ATKlistener to remove.
   */
  public void removeATKlistener(ATKlistener listenerToRemove)
  {
    System.out.println("[Model] - Suppression d'un listener du model");
    this.listeners.remove(ATKlistener.class, listenerToRemove);
  }


  /**
   * Notify the listener that the language changed.
   */
  private void fireLanguageChanged()
  {
    System.out.println("[Model] - Fire LanguageChanged Debut");
    ATKlistener[] listenerList = (ATKlistener[])this.listeners.getListeners(ATKlistener.class);
    for(ATKlistener listener : listenerList)
      listener.LanguageChanged(new LanguageChangedEvent(this, this.getLanguage()));
    System.out.println("[Model] - Fire LanguageChanged Fin");
  }

  /**
   * Notify the listener that the Character's equipments changed.
   */
  private void fireEquipmentChangedEvent()
  {
    System.out.println("[Model] - fireEquipmentChangedEvent Debut");
    ATKlistener[] listenerList = (ATKlistener[])this.listeners.getListeners(ATKlistener.class);
    for(ATKlistener listener : listenerList)
      listener.EquipmentChanged(new EquipmentChangedEvent(this, this.getEquipment()));
    System.out.println("[Model] - fireEquipmentChangedEvent Fin");
  }

  /**
   * Notify the listener that the seletec Item in the item tree changed.
   */
  private void fireItemTypeChangedEvent()
  {
    System.out.println("[Model] - fireItemTypeChangedEvent Debut");
    ATKlistener[] listenerList = (ATKlistener[])this.listeners.getListeners(ATKlistener.class);
    for(ATKlistener listener : listenerList)
      listener.ItemTypeChanged(new ItemTypeChangedEvent(this, this.treeSelectedItem));
    System.out.println("[Model] - fireItemTypeChangedEvent Fin");

  }

  /**
   * Notify the listener that the Character's statistics changed.
   */
  private void fireStatChangedEvent()
  {
    System.out.println("[Model] - fireStatChangedEvent Debut");
    ATKlistener[] listenerList = (ATKlistener[])this.listeners.getListeners(ATKlistener.class);
    for(ATKlistener listener : listenerList)
      listener.StatChanged(new StatChangedEvent(this, this.character.getStat()));
    System.out.println("[Model] - fireStatChangedEvent Fin");
  }
  
  /**
   * Notify the listener that the Character changed.
   */
  private void fireCharacterChangedEvent()
  {
    System.out.println("[Model] - fireCharacterChangedEvent Debut");
    ATKlistener[] listenerList = (ATKlistener[])this.listeners.getListeners(ATKlistener.class);
    for(ATKlistener listener : listenerList)
      listener.CharacterChanged(new CharacterChangedEvent(this, this.character));
    System.out.println("[Model] - fireCharacterChangedEvent Fin");
  }
 
  /**
   * Notify the listener that the Queyr changed.
   */
  private void fireQueryChanged() {
    ATKlistener[] listenerList = (ATKlistener[])this.listeners.getListeners(ATKlistener.class);
    for(ATKlistener listener : listenerList)
      listener.QueryChanged(new QueryChangedEvent(this, this.query));
  }
  
  /**
   * Return a boolean indicating if the model has been modified since its 
   * initialization or the last save.
   * @return Boolean - True is the model changed this its initialization 
   * or the last save, else False;
   */
  public boolean isDirty()
  {
    return this.dirty;
  }

  /**
   * Return the path to the file used to save the Character's data.
   * @return String - The path to the file.
   */
  public String getFilePath()
  {
    return this.filePath;
  }

  /**
   * Setter used to change the path of the file use to store the data.
   * @param newFilePath - The new file path.
   */
  public void setFilePath(String newFilePath)
  {
    this.filePath = newFilePath;
  }

  /**
   * Return the filename used to save the Character's data.
   * @return String - The file name.
   */
  public String getFileName()
  {
    return this.fileName;
  }

  /**
   * Setter used to change the name of the file use to store the data.
   * @param newFileName - The new filename used to store the character's data.
   */
  public void setFileName(String newFileName)
  {
    this.fileName = newFileName;
  }
  /**
   * Return the selected Item in the Item tree.
   * @return Item - The selected Item.
   */
  public Item getSelectedItem()
  {
    return this.treeSelectedItem;
  }

  /**
   * 
   * @param filterCondition - The new Condition 
   */
  public void setQueryCondition(Condition filterCondition)
  {
    this.query.setWhereCondition(filterCondition);
    this.fireQueryChanged();
  }


  /**
   * This method is used to build the Where condition of the Query according
   * to the item selected. Indeed, the Item know which table, the field and 
   * the values must be matched to get all the Item Type from the database.
   * @return Condition - The Where clause generated according to the item
   * selected.
   */
  public Condition buildItemSelectedCondition()
  {
    //Get all the table field values.
    String[] values = treeSelectedItem.getTableFieldValues();
    //Get the field name of the table
    String fieldName = treeSelectedItem.getTableField().trim();
    //Literals used to create an Expression (i.e the condition)
    Literal field, value;
    //The Condition which will be return at the end of the build.
    Condition filterCondition = new Condition();
    //If the name of the field is not empty ...
    if(!fieldName.isEmpty())
    {
      //... then, create an Expression;
      Expression filterItem; 
      //Instanciate a literal according to the fieldName
      field = new Literal(fieldName);
      //For each value that the field must look like ...
      for(String val : values)
      {
        //... instanciate the expression according to the literal "field".
        filterItem = new Expression(field);
        //Create the literal according to the value the field must look like.
        value = new Literal(val);
        //Define the Expression as a Like condition between "field" and "value"
        filterItem.addComparatorAndLiteral(ComparatorEnum.LIKE, value);
        //If the condition does not have a primary expression
        if(filterCondition.getExpression1() == null)
          //... then the Expression is set as the primary expression
          filterCondition.setFirstExpression(filterItem);
        else
          //... else we define it as a next expression.
          filterCondition.addNextExpression(JunctionEnum.OR, filterItem);
      }
    }
    return filterCondition;
  }
  /**
   * Return the Query used to commnunicate with the DataBaseHandler.
   * @return Query - The query.
   */
  public Query getQuery()
  {
    return this.query;
  }
  /**
   * Setter used to replace the query by a new one.
   * @param newQuery - The new Query used to replace the query contains 
   * in this model.
   */
  public void setQuery(Query newQuery) {
    this.query = newQuery;
    this.fireQueryChanged();
  }

  /**
   * @param dirty the dirty to set
   */
  public void setDirty(boolean dirty) {
    this.dirty = dirty;
  }
  
}